<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK IT ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006-2014 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: luofei614 <weibo.com/luofei614>
// +----------------------------------------------------------------------
// $Id$

/**
 * 将Trace信息输出到chrome浏览器的控制器，从而不影响ajax效果和页面的布局。
 * 使用前，你需要先安装 chrome log 这个插件： http://craig.is/writing/chrome-logger。
 * 定义应用的tags.php文件 Application/Common/Conf/tags.php，
 * <code>
 * <?php return array(
 * 'app_end'=>array(
 * 'Behavior\ChromeShowPageTrace'
 * )
 * );
 * </code>
 * 如果trace信息没有正常输出，请查看您的日志。
 * 这是通过http headers和chrome通信，所以要保证在输出trace信息之前不能有
 * headers输出，你可以在入口文件第一行加入代码 ob_start(); 或者配置output_buffering
 */
namespace Behavior;

use Think\Log;

/**
 * 系统行为扩展 页面Trace显示输出
 */
class ChromeShowPageTraceBehavior {
	protected $tracePageTabs = array (
			'BASE' => '基本',
			'FILE' => '文件',
			'INFO' => '流程',
			'ERR|NOTIC' => '错误',
			'SQL' => 'SQL',
			'DEBUG' => '调试' 
	);
	
	// 行为扩展的执行入口必须是run
	public function run(&$params) {
		if (C ( 'SHOW_PAGE_TRACE' ))
			$this->showTrace ();
	}
	
	/**
	 * 显示页面Trace信息
	 * 
	 * @access private
	 */
	private function showTrace() {
		// 系统默认显示信息
		$files = get_included_files ();
		$info = array ();
		foreach ( $files as $key => $file ) {
			$info [] = $file . ' ( ' . number_format ( filesize ( $file ) / 1024, 2 ) . ' KB )';
		}
		$trace = array ();
		$base = array (
				'请求信息' => date ( 'Y-m-d H:i:s', $_SERVER ['REQUEST_TIME'] ) . ' ' . $_SERVER ['SERVER_PROTOCOL'] . ' ' . $_SERVER ['REQUEST_METHOD'] . ' : ' . __SELF__,
				'运行时间' => $this->showTime (),
				'吞吐率' => number_format ( 1 / G ( 'beginTime', 'viewEndTime' ), 2 ) . 'req/s',
				'内存开销' => MEMORY_LIMIT_ON ? number_format ( (memory_get_usage () - $GLOBALS ['_startUseMems']) / 1024, 2 ) . ' kb' : '不支持',
				'查询信息' => N ( 'db_query' ) . ' queries ' . N ( 'db_write' ) . ' writes ',
				'文件加载' => count ( get_included_files () ),
				'缓存信息' => N ( 'cache_read' ) . ' gets ' . N ( 'cache_write' ) . ' writes ',
				'配置加载' => count ( c () ),
				'会话信息' => 'SESSION_ID=' . session_id () 
		);
		// 读取应用定义的Trace文件
		$traceFile = COMMON_PATH . 'Conf/trace.php';
		if (is_file ( $traceFile )) {
			$base = array_merge ( $base, include $traceFile );
		}
		
		$debug = trace ();
		$tabs = C ( 'TRACE_PAGE_TABS', null, $this->tracePageTabs );
		foreach ( $tabs as $name => $title ) {
			switch (strtoupper ( $name )) {
				case 'BASE' : // 基本信息
					$trace [$title] = $base;
					break;
				case 'FILE' : // 文件信息
					$trace [$title] = $info;
					break;
				default : // 调试信息
					$name = strtoupper ( $name );
					if (strpos ( $name, '|' )) { // 多组信息
						$array = explode ( '|', $name );
						$result = array ();
						foreach ( $array as $name ) {
							$result += isset ( $debug [$name] ) ? $debug [$name] : array ();
						}
						$trace [$title] = $result;
					} else {
						$trace [$title] = isset ( $debug [$name] ) ? $debug [$name] : '';
					}
			}
		}
		chrome_debug ( 'TRACE信息:' . __SELF__, 'group' );
		// 输出日志
		foreach ( $trace as $title => $log ) {
			'错误' == $title ? chrome_debug ( $title, 'group' ) : chrome_debug ( $title, 'groupCollapsed' );
			foreach ( $log as $i => $logstr ) {
				chrome_debug ( $i . '.' . $logstr, 'log' );
			}
			chrome_debug ( '', 'groupEnd' );
		}
		chrome_debug ( '', 'groupEnd' );
		if ($save = C ( 'PAGE_TRACE_SAVE' )) { // 保存页面Trace日志
			if (is_array ( $save )) { // 选择选项卡保存
				$tabs = C ( 'TRACE_PAGE_TABS', null, $this->tracePageTabs );
				$array = array ();
				foreach ( $save as $tab ) {
					$array [] = $tabs [$tab];
				}
			}
			$content = date ( '[ c ]' ) . ' ' . get_client_ip () . ' ' . $_SERVER ['REQUEST_URI'] . "\r\n";
			foreach ( $trace as $key => $val ) {
				if (! isset ( $array ) || in_array ( $key, $array )) {
					$content .= '[ ' . $key . " ]\r\n";
					if (is_array ( $val )) {
						foreach ( $val as $k => $v ) {
							$content .= (! is_numeric ( $k ) ? $k . ':' : '') . print_r ( $v, true ) . "\r\n";
						}
					} else {
						$content .= print_r ( $val, true ) . "\r\n";
					}
					$content .= "\r\n";
				}
			}
			error_log ( str_replace ( '<br/>', "\r\n", $content ), 3, LOG_PATH . date ( 'y_m_d' ) . '_trace.log' );
		}
		unset ( $files, $info, $base );
	}
	
	/**
	 * 获取运行时间
	 */
	private function showTime() {
		// 显示运行时间
		G ( 'beginTime', $GLOBALS ['_beginTime'] );
		G ( 'viewEndTime' );
		// 显示详细运行时间
		return G ( 'beginTime', 'viewEndTime' ) . 's ( Load:' . G ( 'beginTime', 'loadTime' ) . 's Init:' . G ( 'loadTime', 'initTime' ) . 's Exec:' . G ( 'initTime', 'viewStartTime' ) . 's Template:' . G ( 'viewStartTime', 'viewEndTime' ) . 's )';
	}
}
if (! function_exists ( 'chrome_debug' )) {
	// ChromePhp 输出trace的函数
	function chrome_debug($msg, $type = 'trace', $trace_level = 1) {
		if ('trace' == $type) {
			ChromePhp::groupCollapsed ( $msg );
			$traces = debug_backtrace ( false );
			$traces = array_reverse ( $traces );
			$max = count ( $traces ) - $trace_level;
			for($i = 0; $i < $max; $i ++) {
				$trace = $traces [$i];
				$fun = isset ( $trace ['class'] ) ? $trace ['class'] . '::' . $trace ['function'] : $trace ['function'];
				$file = isset ( $trace ['file'] ) ? $trace ['file'] : 'unknown file';
				$line = isset ( $trace ['line'] ) ? $trace ['line'] : 'unknown line';
				$trace_msg = '#' . $i . '  ' . $fun . ' called at [' . $file . ':' . $line . ']';
				if (! empty ( $trace ['args'] )) {
					ChromePhp::groupCollapsed ( $trace_msg );
					ChromePhp::log ( $trace ['args'] );
					ChromePhp::groupEnd ();
				} else {
					ChromePhp::log ( $trace_msg );
				}
			}
			ChromePhp::groupEnd ();
		} else {
			if (method_exists ( 'Behavior\ChromePhp', $type )) {
				// 支持type trace，warn,log,error,group, groupCollapsed, groupEnd等
				call_user_func ( array (
						'Behavior\ChromePhp',
						$type 
				), $msg );
			} else {
				// 如果type不为trace,warn,log等，则为log的标签
				call_user_func_array ( array (
						'Behavior\ChromePhp',
						'log' 
				), func_get_args () );
			}
		}
	}
	
	/**
	 * Server Side Chrome PHP debugger class
	 *
	 * @package ChromePhp
	 * @author Craig Campbell <iamcraigcampbell@gmail.com>
	 */
	class ChromePhp {
		/**
		 *
		 * @var string
		 */
		const VERSION = '4.1.0';
		
		/**
		 *
		 * @var string
		 */
		const HEADER_NAME = 'X-ChromeLogger-Data';
		
		/**
		 *
		 * @var string
		 */
		const BACKTRACE_LEVEL = 'backtrace_level';
		
		/**
		 *
		 * @var string
		 */
		const LOG = 'log';
		
		/**
		 *
		 * @var string
		 */
		const WARN = 'warn';
		
		/**
		 *
		 * @var string
		 */
		const ERROR = 'error';
		
		/**
		 *
		 * @var string
		 */
		const GROUP = 'group';
		
		/**
		 *
		 * @var string
		 */
		const INFO = 'info';
		
		/**
		 *
		 * @var string
		 */
		const GROUP_END = 'groupEnd';
		
		/**
		 *
		 * @var string
		 */
		const GROUP_COLLAPSED = 'groupCollapsed';
		
		/**
		 *
		 * @var string
		 */
		const TABLE = 'table';
		
		/**
		 *
		 * @var string
		 */
		protected $_php_version;
		
		/**
		 *
		 * @var int
		 */
		protected $_timestamp;
		
		/**
		 *
		 * @var array
		 */
		protected $_json = array (
				'version' => self::VERSION,
				'columns' => array (
						'log',
						'backtrace',
						'type' 
				),
				'rows' => array () 
		);
		
		/**
		 *
		 * @var array
		 */
		protected $_backtraces = array ();
		
		/**
		 *
		 * @var bool
		 */
		protected $_error_triggered = false;
		
		/**
		 *
		 * @var array
		 */
		protected $_settings = array (
				self::BACKTRACE_LEVEL => 1 
		);
		
		/**
		 *
		 * @var ChromePhp
		 */
		protected static $_instance;
		
		/**
		 * Prevent recursion when working with objects referring to each other
		 *
		 * @var array
		 */
		protected $_processed = array ();
		
		/**
		 * constructor
		 */
		private function __construct() {
			$this->_php_version = phpversion ();
			$this->_timestamp = $this->_php_version >= 5.1 ? $_SERVER ['REQUEST_TIME'] : time ();
			$this->_json ['request_uri'] = $_SERVER ['REQUEST_URI'];
		}
		
		/**
		 * gets instance of this class
		 *
		 * @return ChromePhp
		 */
		public static function getInstance() {
			if (self::$_instance === null) {
				self::$_instance = new self ();
			}
			return self::$_instance;
		}
		
		/**
		 * logs a variable to the console
		 *
		 * @param mixed $data,...
		 *        	unlimited OPTIONAL number of additional logs [...]
		 * @return void
		 */
		public static function log() {
			$args = func_get_args ();
			return self::_log ( '', $args );
		}
		
		/**
		 * logs a warning to the console
		 *
		 * @param mixed $data,...
		 *        	unlimited OPTIONAL number of additional logs [...]
		 * @return void
		 */
		public static function warn() {
			$args = func_get_args ();
			return self::_log ( self::WARN, $args );
		}
		
		/**
		 * logs an error to the console
		 *
		 * @param mixed $data,...
		 *        	unlimited OPTIONAL number of additional logs [...]
		 * @return void
		 */
		public static function error() {
			$args = func_get_args ();
			return self::_log ( self::ERROR, $args );
		}
		
		/**
		 * sends a group log
		 *
		 * @param
		 *        	string value
		 */
		public static function group() {
			$args = func_get_args ();
			return self::_log ( self::GROUP, $args );
		}
		
		/**
		 * sends an info log
		 *
		 * @param mixed $data,...
		 *        	unlimited OPTIONAL number of additional logs [...]
		 * @return void
		 */
		public static function info() {
			$args = func_get_args ();
			return self::_log ( self::INFO, $args );
		}
		
		/**
		 * sends a collapsed group log
		 *
		 * @param
		 *        	string value
		 */
		public static function groupCollapsed() {
			$args = func_get_args ();
			return self::_log ( self::GROUP_COLLAPSED, $args );
		}
		
		/**
		 * ends a group log
		 *
		 * @param
		 *        	string value
		 */
		public static function groupEnd() {
			$args = func_get_args ();
			return self::_log ( self::GROUP_END, $args );
		}
		
		/**
		 * sends a table log
		 *
		 * @param
		 *        	string value
		 */
		public static function table() {
			$args = func_get_args ();
			return self::_log ( self::TABLE, $args );
		}
		
		/**
		 * internal logging call
		 *
		 * @param string $type        	
		 * @return void
		 */
		protected static function _log($type, array $args) {
			// nothing passed in, don't do anything
			if (count ( $args ) == 0 && $type != self::GROUP_END) {
				return;
			}
			
			$logger = self::getInstance ();
			
			$logger->_processed = array ();
			
			$logs = array ();
			foreach ( $args as $arg ) {
				$logs [] = $logger->_convert ( $arg );
			}
			
			$backtrace = debug_backtrace ( false );
			$level = $logger->getSetting ( self::BACKTRACE_LEVEL );
			
			$backtrace_message = 'unknown';
			if (isset ( $backtrace [$level] ['file'] ) && isset ( $backtrace [$level] ['line'] )) {
				$backtrace_message = $backtrace [$level] ['file'] . ' : ' . $backtrace [$level] ['line'];
			}
			
			$logger->_addRow ( $logs, $backtrace_message, $type );
		}
		
		/**
		 * converts an object to a better format for logging
		 *
		 * @param
		 *        	Object
		 * @return array
		 */
		protected function _convert($object) {
			// if this isn't an object then just return it
			if (! is_object ( $object )) {
				return $object;
			}
			
			// Mark this object as processed so we don't convert it twice and it
			// Also avoid recursion when objects refer to each other
			$this->_processed [] = $object;
			
			$object_as_array = array ();
			
			// first add the class name
			$object_as_array ['___class_name'] = get_class ( $object );
			
			// loop through object vars
			$object_vars = get_object_vars ( $object );
			foreach ( $object_vars as $key => $value ) {
				
				// same instance as parent object
				if ($value === $object || in_array ( $value, $this->_processed, true )) {
					$value = 'recursion - parent object [' . get_class ( $value ) . ']';
				}
				$object_as_array [$key] = $this->_convert ( $value );
			}
			
			$reflection = new ReflectionClass ( $object );
			
			// loop through the properties and add those
			foreach ( $reflection->getProperties () as $property ) {
				
				// if one of these properties was already added above then ignore it
				if (array_key_exists ( $property->getName (), $object_vars )) {
					continue;
				}
				$type = $this->_getPropertyKey ( $property );
				
				if ($this->_php_version >= 5.3) {
					$property->setAccessible ( true );
				}
				
				try {
					$value = $property->getValue ( $object );
				} catch ( ReflectionException $e ) {
					$value = 'only PHP 5.3 can access private/protected properties';
				}
				
				// same instance as parent object
				if ($value === $object || in_array ( $value, $this->_processed, true )) {
					$value = 'recursion - parent object [' . get_class ( $value ) . ']';
				}
				
				$object_as_array [$type] = $this->_convert ( $value );
			}
			return $object_as_array;
		}
		
		/**
		 * takes a reflection property and returns a nicely formatted key of the property name
		 *
		 * @param
		 *        	ReflectionProperty
		 * @return string
		 */
		protected function _getPropertyKey(ReflectionProperty $property) {
			$static = $property->isStatic () ? ' static' : '';
			if ($property->isPublic ()) {
				return 'public' . $static . ' ' . $property->getName ();
			}
			
			if ($property->isProtected ()) {
				return 'protected' . $static . ' ' . $property->getName ();
			}
			
			if ($property->isPrivate ()) {
				return 'private' . $static . ' ' . $property->getName ();
			}
		}
		
		/**
		 * adds a value to the data array
		 *
		 * @var mixed
		 * @return void
		 */
		protected function _addRow(array $logs, $backtrace, $type) {
			// if this is logged on the same line for example in a loop, set it to null to save space
			if (in_array ( $backtrace, $this->_backtraces )) {
				$backtrace = null;
			}
			
			// for group, groupEnd, and groupCollapsed
			// take out the backtrace since it is not useful
			if ($type == self::GROUP || $type == self::GROUP_END || $type == self::GROUP_COLLAPSED) {
				$backtrace = null;
			}
			
			if ($backtrace !== null) {
				$this->_backtraces [] = $backtrace;
			}
			
			$row = array (
					$logs,
					$backtrace,
					$type 
			);
			
			$this->_json ['rows'] [] = $row;
			$this->_writeHeader ( $this->_json );
		}
		protected function _writeHeader($data) {
			header ( self::HEADER_NAME . ': ' . $this->_encode ( $data ) );
		}
		
		/**
		 * encodes the data to be sent along with the request
		 *
		 * @param array $data        	
		 * @return string
		 */
		protected function _encode($data) {
			return base64_encode ( utf8_encode ( json_encode ( $data ) ) );
		}
		
		/**
		 * adds a setting
		 *
		 * @param
		 *        	string key
		 * @param
		 *        	mixed value
		 * @return void
		 */
		public function addSetting($key, $value) {
			$this->_settings [$key] = $value;
		}
		
		/**
		 * add ability to set multiple settings in one call
		 *
		 * @param array $settings        	
		 * @return void
		 */
		public function addSettings(array $settings) {
			foreach ( $settings as $key => $value ) {
				$this->addSetting ( $key, $value );
			}
		}
		
		/**
		 * gets a setting
		 *
		 * @param
		 *        	string key
		 * @return mixed
		 */
		public function getSetting($key) {
			if (! isset ( $this->_settings [$key] )) {
				return null;
			}
			return $this->_settings [$key];
		}
	}
}
